﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using AdAggregator.Advertisement;
using AdAggregator.Parser;
using AdAggregator.SearchData;
using AdAggregator.Util;
using AdAggregator.Website;
using log4net;

namespace AdAggregator.Aggregator
{
    public abstract class BaseAggregator<TSearchData, TAdvertisement> : IAggregator<TSearchData, TAdvertisement>
        where TSearchData : ISearchData
        where TAdvertisement : IAdvertisement
    {
        private readonly ILog log = LogManager.GetLogger(typeof(BaseAggregator<TSearchData, TAdvertisement>));
        public IEnumerable<TAdvertisement> Results { get; set; }
        public long AllAvailable { get; set; }
        public long PageCount { get; set; }
        public int ResultsPerPage { get; set; }
        public int PagesInAdvance { get; set; }
        public bool IsInversedSearch { get; set; }
        private IParseResult<TAdvertisement> aggregatedResults;

        protected BaseAggregator()
        {
            ResultsPerPage = 10;
            PagesInAdvance = 2;
            Results = new List<TAdvertisement>();
            aggregatedResults = new ParseResult<TAdvertisement>();
        }

        public abstract IEnumerable<IWebsite<TSearchData, TAdvertisement>> GetWebsites();

        public virtual IEnumerable<PropertyInfo> GetSortableProperties()
        {
            return
                typeof(TAdvertisement).GetProperties()
                                       .Where(prop => prop.IsDefined(typeof(Attribute.Sortable), false));
        }

        public virtual void Aggregate(TSearchData searchData, bool isInitialSearch)
        {
            if (isInitialSearch)
            {
                Clear();
                InitialSearch(searchData, false);
            }
            else
            {
                PageBasedSearch(searchData);
            }
            GetResultPage(searchData);
        }

        protected void PageBasedSearch(TSearchData searchData)
        {
            var lastPage = (int)Math.Ceiling(AllAvailable / (double)ResultsPerPage);
            if ((searchData.Page == 1 && IsInversedSearch) || (searchData.Page == lastPage && !IsInversedSearch && aggregatedResults.Results.Count <= ResultsPerPage * (searchData.Page - 1)))
            {
                IsInversedSearch = searchData.Page == lastPage;
                InitialSearch(searchData, IsInversedSearch);
            }
            else
            {
                if (IsInversedSearch)
                {
                    searchData.Page = lastPage - searchData.Page + 1;
                }
                var requiredAds = ResultsPerPage * searchData.Page;
                if (aggregatedResults.Results.Count < requiredAds && aggregatedResults.AllAvailable >= requiredAds)
                {
                    DownloadMissingPage(searchData);
                }
                else if (aggregatedResults.Results.Count < aggregatedResults.AllAvailable
                         && aggregatedResults.Results.Count < (searchData.Page + PagesInAdvance) * ResultsPerPage)
                {
                    DownloadPagesInAdvance(searchData);
                }
            }
        }

        protected void DownloadPagesInAdvance(TSearchData searchData)
        {
            log.Debug(String.Format("Page: {0}, Downloaded ads: {1}, Starting task", searchData.Page,
                                    aggregatedResults.Results.Count));
            Task.Run(() =>
                {
                    var taskSearchData = (TSearchData)searchData.Copy();
                    taskSearchData.Page = aggregatedResults.LastSearchPage + 1;
                    var taskResult = GetResults(taskSearchData, IsInversedSearch, aggregatedResults);
                    if (taskSearchData.Page > taskResult.LastSearchPage)
                    {
                        aggregatedResults.Results = taskResult.Results;
                        aggregatedResults.LastSearchPage = taskSearchData.Page;
                    }
                    log.Debug(String.Format("Page: {0}, Downloaded ads: {1}, Task result", searchData.Page,
                                            aggregatedResults.Results.Count));
                });
        }

        protected void DownloadMissingPage(TSearchData searchData)
        {
            var searchDataCopy = (TSearchData)searchData.Copy();
            searchDataCopy.Page = aggregatedResults.LastSearchPage + 1;
            aggregatedResults = GetResults(searchDataCopy, IsInversedSearch, aggregatedResults);
            if (searchDataCopy.Page > aggregatedResults.LastSearchPage)
            {
                aggregatedResults.LastSearchPage = searchDataCopy.Page;
            }
        }

        protected void InitialSearch(TSearchData searchData, bool isInversed)
        {
            searchData.Page = 1;
            var parseResult = GetResults(searchData, isInversed);
            parseResult.LastSearchPage = 1;
            aggregatedResults = parseResult;
        }

        protected void GetResultPage(TSearchData searchData)
        {
            if (aggregatedResults.AllAvailable > ResultsPerPage)
            {
                var lastPage = (int)Math.Ceiling(aggregatedResults.AllAvailable / (double)ResultsPerPage);
                var pageResult = aggregatedResults.Results.Skip(ResultsPerPage * (searchData.Page - 1)).Take(ResultsPerPage).ToList();
                if (IsInversedSearch && searchData.Page == 1)
                {
                    searchData.SortingDirection = GetInversedDirection(searchData.SortingDirection);
                }
                Results = SortResults(searchData, pageResult);
                AllAvailable = aggregatedResults.AllAvailable;
                PageCount = lastPage;
                if (IsInversedSearch) searchData.Page = lastPage - searchData.Page + 1;
            }
            else
            {
                Results = aggregatedResults.Results;
            }
        }

        protected IParseResult<TAdvertisement> GetResults(TSearchData searchData, bool isInversed, IParseResult<TAdvertisement> prevResult = null)
        {
            var parseResult = prevResult ?? new ParseResult<TAdvertisement>();
            if (isInversed)
            {
                searchData.SortingDirection = GetInversedDirection(searchData.SortingDirection);
            }
            var newResult = DownloadResults(searchData);
            parseResult.AllAvailable = newResult.AllAvailable;
            parseResult.Results.AddRange(newResult.Results);
            parseResult = DuplicateRemover<TAdvertisement>.GetResultWithoutDuplicates(parseResult);
            parseResult.Results = SortResults(searchData, parseResult.Results);
            return parseResult;
        }

        protected IParseResult<TAdvertisement> DownloadResults(TSearchData searchData)
        {
            var parseResult = new ParseResult<TAdvertisement>();
            Parallel.ForEach(GetWebsites(), site =>
            {
                var singleResult = site.Process(searchData);
                parseResult.Results.AddRange(singleResult.Results);
                parseResult.AllAvailable += singleResult.AllAvailable;
            });
            return parseResult;
        }

        protected List<TAdvertisement> SortResults(ISearchData searchData, List<TAdvertisement> unsortedResults)
        {
            var prop = typeof(TAdvertisement).GetProperty(searchData.OrderBy);
            var emptyPropAds = unsortedResults.Where(myAd => prop.GetValue(myAd) == null).ToList();
            unsortedResults.RemoveAll(myAd => prop.GetValue(myAd) == null);
            var result = searchData.SortingDirection == SortingDirection.ASC
                             ? unsortedResults.OrderBy(ad => prop.GetValue(ad)).ToList()
                             : unsortedResults.OrderByDescending(ad => prop.GetValue(ad)).ToList();
            result.AddRange(emptyPropAds);
            return result;
        }

        protected SortingDirection GetInversedDirection(SortingDirection originalDirection)
        {
            return originalDirection == SortingDirection.ASC ? SortingDirection.DESC : SortingDirection.ASC;
        }

        protected void Clear()
        {
            Results = new List<TAdvertisement>();
            AllAvailable = 0;
            PageCount = 0;
            IsInversedSearch = false;
            aggregatedResults = null;
        }
    }
}
